集合学习和随机森林(Ensemble Learning and Random Forests)

假设你向成千上万个随机人提出一个复杂的问题,然后汇总他们的答案。在许多情况下,你会发现这个汇总的答案比专家的答案更好。这被称为人群的智慧。同样,对于一组预测样本,汇总一批预测器的预测结果,往往要好于一个单独的预测器。这一组预测器称为集成(ensemble),这种技术称为集成学习,一个集合学习算法称为集成方法

随机森林就是一种集成学习,其中包含了多个决策树。例如,你希望用决策树做一个分类器,每个子树都在训练集不同的随机子集上训练。每个子树都有自己的预测结果,决策树的将所有单个树的预测的预测结果统计,然后选择最多选票的类别(参见第6章的最后一个练习)。这样的决策树集合被称为随机森林,尽管它很简单,但它是当今最强大的机器学习算法之一。

一旦你已经构建了一些好的预测器,你就会经常在项目使用Ensemble方法,将它们组合成一个更好的预测器。 事实上,机器学习竞赛中获胜的解决方案通常涉及多种Ensemble方法)。

在本文,我们除了讨论随机森林,也将讨论最流行的集成方法,包括bagging, boosting, stacking等。

投票分类器(Voting Classifiers)

假设我们已经训练了一些分类器,每个分类器的准确率达到约80%。你可能有一个Logistic回归分类器,一个SVM分类器,一个随机森林分类器,K-Nearest Neighbors分类器,也许还有一些别的

创建一个更好的分类器的一种非常简单的方法是聚合每个分类器的预测结果并选择获得最多选票的类作为预测结果。这种多数表决分类器称为硬投票分类器。而且这种投票分类器通常比集合中的最佳分类器具有更高的准确度。事实上,即使每个分类器都是弱学习者(意味着它只比随机猜测稍微好一些),整体仍然可以成为一个强大的学习器(达到高准确度),只要有足够数量的弱学习者并且他们足够多样化。

这是为什么呢? 以下类比可以帮助揭示这个问题的答案。假设你有一个不均匀的硬币,有51%的机会出现正面,并有49%的机会出现反面。如果扔1000次(1000个训练样本),通常会得到或多或少510正和490反,因此大多数是正。如果你有10个这样的硬币(10个预测器),每个都扔1000次,大多数硬币出现正面的次数会更多。如果你有10,000个这样的硬币,我们每个抛100次,正面出现多的硬币数量/硬币总数 将接近于97%(集成学习的预测准确率将达到97%)。

这是由于大数定律:当你不停地掷硬币时,正面的比例越来越接近51%。本身只是准确率仅仅为51%的弱分类器,10000多个联合在一起竟然也可以实现准确率为97%的强分类器,可以说是非常令人惊讶了!但是,这里有个前提,即每个分类器必须完全独立,才可以达到理论的最高水平。换言之,我们希望不同的分类器有不同的关注点,从不同的角度分类数据。使用完全不同的算法训练数据是一种有效的集成学习方法,这增加了他们制造完全不同类型错误的机会,从而提高了整体的准确性。

以下代码在Scikit-Learn中创建并训练投票分类器,包含三种不同的分类器(训练集是moons dataset):

投票分类器略微优于所有单独的分类器。

如果所有分类器都能够估计类概率(即有一个predict_proba()方法),那么你可以告诉Scikit-Learn预测具有最高等级概率的类,对所有单个分类器进行平均。这称为软投票。 它通常比硬投票获得更高的性能,因为它给予高度自信的投票更多的权重。你需要做的就是用voting =“soft”替换voting =“hard”,并确保所有分类器都可以估计类概率。默认情况下,SVC类不是估计概率,因此需要将其概率超参数设置为True (这将使SVC类使用交叉验证来估计类概率,减慢训练,并且它将添加predict_proba()方法)。如果修改前面的代码以使用软投票,您会发现投票分类器的准确率达到91%以上!

Bagging and Pasting

正如刚才所讲,集成学习效果好的前提是每个预测器是独立的。但是这也是有缺陷的,我们未必能找到这么多独立的预测器,如何在数量有限的预测器中,构建集成学习器呢? 答案是用训练集的子集训练每个预测器。 如果一个训练集能分出 n 个训练子集,每个预测器在这 n 个子集都完成了训练,那么意味着预测器的数量翻了 n 倍。我们通过这种方式增加了预测器的数量,解决了集成学习预测器数量不足的问题。

那么如何分数据就是我们需要考虑的了。有以下两种思路可供选择:Bagging和Pasting。

举个例子:500个数据样本,计划每个子集为100个。 Bagging先随机拿出100个样本,训练现有的预测器(有SVM,逻辑回归器等等),训练完,将这100个样本再丢回原先的数据集,再次抽取100个随机样本。 Pasting则拿出100个样本,训练完,再从剩下的400个训练样本中抽取。

Bagging and Pasting

Scikit-Learn提供了一个简单的API使用BaggingClassifier类(或 BaggingRegressor 进行回归)进行Bagging and Pasting。以下代码训练500个决策树分类器的集合。如果你想使用Pasting,只需设置bootstrap = False。

参数 n_jobs告诉Scikit-Learn用于训练和预测的CPU核心数( -1 告诉Scikit-Learn使用所有可用核心)

左图是一个决策树的决策边界(显然有点过拟合),右边是使用500棵决策树的Bagging集合的决策边界(很圆润的边界)。如果我们进一步描述的话,可以认为集成学习的结果方差较小从而使整体决策边界很“圆润”。

Bagging 和 Pasting方法为每个训练集引用了更多的多样性,但是大家更多的采用bagging方法,因为它更方便。

Out-of-Bag Evaluation

对于任何给定的预测器,有些实例可能会被采样而其他实例可能根本没有被抽中。未采样的训练实例称为out-of-bag (oob)实例。

由于预测器在训练期间从不会看到 oob 实例,因此可以在这些实例上进行评估,无需单独的验证集或交叉验证。你可以通过平均每个预测器的 oob 评估来评估整体本身。

在Scikit-Learn中,你可以在创建BaggingClassifier时设置oob_score = True要求在训练后进行自动oob评估。 以下代码演示了这一点。最终的评估分数可通过oob_score_变量获得:

根据这个oob评估,BaggingClassifier可能在测试集上达到约91.2%的准确度。 我们来验证一下:

每个训练实例的oob决策特征也可以通过oob_decision_function_变量获得。在这种情况下(由于基类估计器具有predict_proba()方法),决策函数返回每个训练实例的类概率。例如,oob评估估计第二个训练实例有65.89%属于positive class的概率(和属于negative class的34.11%):

随机子空间方法

对训练实例和特征进行采样称为Random Patches。 对特征采样被称为随机子空间方法。

采样特征导致预测器更多的多样性。

随机森林(Random Forests)

随机森林是决策树的集合,通常通过Bagging方法(或有时Pasting)训练。 你可以改为使用RandomForestClassifier类,而不是构建BaggingClassifier并将其传递给DecisionTreeClassifier,这样更方便并针对Decision Trees 进行了优化 (类似地,有一个RandomForestRegressor类用于回归任务)。以下代码使用所有可用的CPU核心训练具有500棵树(每个限制为最多16个节点)的随机森林分类器:

极端树(Extra-Trees)

前文提到,在随机森林的基础上,特征也可以随机提取,对每个特征使用随机阈值而不是搜索最佳可能阈值(如常规决策树那样)时,可以使树更加随机。 即决策树多样 + 数据集随机 + 特征随机 这种树称为极端随机树(Extremely Randomized Trees ensemble) 简称Extra-Trees。这种策略会导致每个决策树之间方差减小,偏差变大。Extra-Trees比常规随机森林更快地训练。

可以使用Scikit-Learn的ExtraTreesClassifier类创建Extra-Trees分类器。它的API与RandomForestClassifier类相同。同样,Extra TreesRegressor 类与RandomForestRegressor类具有相同的API。

事先很难判断RandomForestClassifier的性能是否比ExtraTreesClassifier更好或更差。通常,要知道的唯一方法是尝试两者并使用交叉验证(同时使用网格搜索调整超参数)来比较它们。

特征重要性(Feature Importance)

如果你查看一个决策树可能会发现,重要特征可能看起来更接近树的根,而不重要的特征通常会更接近叶子(或根本不显示)。因此,可以通过计算特征在森林中所有树木中出现的平均深度来估计特征的重要性。Scikit-Learn会在训练后自动为每个特征计算。 你可以使用feature_importances_变量访问结果。

例如,以下代码训练鸢尾花数据集上的RandomForestClassifier(在第4章中介绍)并输出每个特征的重要性。似乎最重要的特征是花瓣长度(44%)和宽度(42%),而萼片的长度和宽度相比较不重要(分别为11%和2%):

提升(Boosting)

提升(Boosting,最初称为假设增强)指的是可以将几个弱学习者组合成强学习者的集成方法。对于大多数的提升方法的思想就是按顺序去训练分类器,每一个都要尝试修正前面的分类。现如今已经有很多的提升方法了,但最著名的就是 Adaboost(适应性提升,是 Adaptive Boosting 的简称) 和 Gradient Boosting(梯度提升)。让我们先从 Adaboost 说起。

AdaBoost

新预测器纠正其前身的一种方法是更多地关注之前的预测器欠拟合的训练实例。 这导致新的预测器越来越关注比较难的实例。这是AdaBoost使用的技术。

举个例子,去构建一个 Adaboost 分类器,先训练第一个基类分类器(例如一个决策树),然后拿来在训练集上做预测,然后增加错误分类的训练实例的相对权重。 使用更新后的权重训练第二个分类器,并再次对训练集进行预测,更新权重,以此类推。

以下代码使用Scikit-Learn的AdaBoostClassifier类训练基于200个决策树桩的AdaBoost分类器(正如你所料,还有一个AdaBoostRegressor类)。决策树桩是具有max_depth = 1的决策树 ——换句话说,是由单个决策节点加上两个叶节点组成的树。 这是AdaBoostClassifier类的默认基类估算器:

如果你的AdaBoost集合过拟合训练集,您可以尝试减少估算器的数量,或者强调更正则化的预测器。

梯度提升(Gradient Boosting)

另一个非常流行的Boosting算法是Gradient Boosting。就像AdaBoost一样,Gradient Boosting通过在一个集合中依次添加预测器来进行工作,每一个都纠正它的前任。 但是,这种方法不是像AdaBoost那样在每次迭代时调整实例权重,而是尝试将新预测器拟合到先前预测器所产生的残差

我们通过一个简单的回归示例,使用决策树作为基础预测器(当然,梯度提升也适用于回归任务)。 这称为梯度提升(Gradient Tree Boosting)或梯度提升回归树(Gradient Boosted Regression Trees)(GBRT)。 首先,让DecisionTreeRegressor拟合训练集

现在训练第二个DecisionTreeRegressor来解决第一个预测器遗留的残差:

然后我们训练第三个DecisionTreeRegressor来处理第二个预测器产生的残差:

现在我们有一个包含三棵树的集合。它可以简单地通过累加所有树的预测来对新实例进行预测:

上图表示左列中这三棵树的预测,以及右栏中集合的预测。

训练GBRT集合的一种更简单的方法是使用Scikit-Learn的GradientBoostingRegressor类。与RandomForestRegressor类非常相似,它具有:

learning_rate超参数缩放每棵树的贡献。如果将其设置为较低的值,例如0.1,你需要更多的树木才能适应训练集,但预测通常会更好地概括。 这是一种称为shrinkage的正则化技术。

上图显示了以低学习率训练的两个GBRT集合:左边的那个没有足够的树来拟合训练集,而右边的那个有太多的树并且过拟合训练集。

早停策略

为了找到最佳树木数量,您可以使用提前停止-Early stopping。 实现这一点的一种简单方法是使用staged_predict()方法:它返回一个迭代器,该迭代器覆盖整个训练阶段的集合所做的预测(有一棵树,两棵树等)。

以下代码训练带有120棵树的GBRT集合,然后测量每个训练阶段的验证误差以找到最佳树木数量,最后使用最佳树木数量训练另一个GBRT集合:

验证错误显示在图的左侧,最佳模型的预测显示在右侧。

实际中也可以通过早期停止训练来实现早期停止(而不是先培养大量树木,然后回头寻找最佳数量)。你可以通过设置warm_start = True来实现,这使得Scikit-Learn在调用fit()方法时保留现有树,从而允许增量训练。 当验证错误连续五次迭代没有改进时,以下代码停止训练:

GradientBoostingRegressor类还支持子样本超参数,它指定用于训练每棵树的训练实例的百分数。例如,如果subsample = 0.25,则每个树在25%的训练实例上进行训练,随机选择。 正如你现在可能猜到的那样,对于较低的方差,这会产生较高的偏差。 它还大大加快了训练速度。 这种技术称为随机梯度增强(Stochastic Gradient Boosting)。

可以使用Gradient Boosting和其他成本函数。这由损失超参数控制(有关详细信息,请参阅Scikit-Learn的文档)。

Stacking

我们在本文讨论的最后一个Ensemble方法称为堆叠(叠加泛化的简称)。它基于一个简单的想法:为什么我们不训练模型来执行这种聚合?而不是使用简单的函数(如硬投票)来聚合集合中所有预测器的预测, 图7-12显示了这样一个集合在新实例上执行回归任务。

为了训练混合器或元学习器,一种常见的方法是使用hold-out set。让我们看看它是如何工作的。

首先,训练集分为两个子集。 第一个子集用于训练第一层中的预测器(有三个)(见图7-13)。

接下来,第一层预测器被用于在第二个子集进行预测(hold-out)(见图7-14)。 这可以确保预测“干净”,因为预测器在训练期间从未见过这些实例。

现在,对于 hold-out 集中的每个实例,有三个预测值。 我们可以使用这些预测值作为输入特征(这使得这个新的训练集为三维)创建一个新的训练集,并保持目标值。 混合器或元学习器在这个新的训练集上训练,因此它基于第一层的预测结果,学习预测目标值。

实际上可以用这种方式训练几种不同的搅拌器(例如,一个使用线性回归,另一个使用随机森林回归,等等):我们得到一整层搅拌器。 诀窍是将训练集分成三个子集:

不幸的是,Scikit-Learn不支持直接堆叠,但是推出自己的实现并不太难(参见以下练习)。 或者,你可以使用Brew等开源实现 (可在[https://github.com/viisar/brew] 获得)。

值得注意的是,可以使用Gradient Boosting的优化实现在流行的python库XGBoost中,该库代表Extreme Gradient Boosting。

此软件包最初是由陈天奇作为Distributed(Deep)的一部分开发的机器学习社区(DMLC),旨在实现极快,可扩展的 和便携式。 实际上,XGBoost通常是获胜的重要组成部分ML竞赛中的参赛模型。 XGBoost的API与Scikit-Learn的API非常相似: